/**
 * Copyright (C) 2012-2015 Yecheng Fu <cofyc.jackson at gmail dot com>
 * All rights reserved.
 *
 * Use of this source code is governed by a MIT-style license that can be found
 * in the LICENSE file.
 */
#include "argparse.h"
#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define OPT_UNSET 1
#define OPT_LONG (1 << 1)
#if defined(_WIN32) || defined(_WIN64)
#include <wchar.h>
wchar_t *yk__utf8_to_utf16_null_terminated(const char *str);
#define fprintf fwprintf
#define fputc fputwc
#define str_literal(t) L##t
#else
#define str_literal(t) t
#endif
static const char *prefix_skip(const char *str, const char *prefix) {
  size_t len = strlen(prefix);
  return strncmp(str, prefix, len) ? NULL : str + len;
}
static int prefix_cmp(const char *str, const char *prefix) {
  for (;; str++, prefix++)
    if (!*prefix) {
      return 0;
    } else if (*str != *prefix) {
      return (unsigned char) *prefix - (unsigned char) *str;
    }
}
static void argparse_error(struct argparse *self,
                           const struct argparse_option *opt,
                           const char *reason, int flags) {
  (void) self;
  if (flags & OPT_LONG) {
#if defined(_WIN32) || defined(_WIN64)
    wchar_t *long_name_copy = yk__utf8_to_utf16_null_terminated(opt->long_name);
    wchar_t *reason_copy = yk__utf8_to_utf16_null_terminated(reason);
    if (long_name_copy != NULL && reason_copy != NULL) {
      fprintf(stderr, str_literal("error: option `--%ls` %ls\n"), long_name_copy,
              reason_copy);
    }
    free(long_name_copy);
    free(reason_copy);
#else
    fprintf(stderr, str_literal("error: option `--%s` %s\n"), opt->long_name,
            reason);
#endif
  } else {
#if defined(_WIN32) || defined(_WIN64)
    wchar_t *reason_copy = yk__utf8_to_utf16_null_terminated(reason);
    if (reason_copy != NULL) {
      fprintf(stderr, str_literal("error: option `-%c` %ls\n"), opt->short_name,
              reason_copy);
    }
    free(reason_copy);
#else
    fprintf(stderr, str_literal("error: option `-%c` %s\n"), opt->short_name,
            reason);
#endif
  }
  exit(EXIT_FAILURE);
}
static int argparse_getvalue(struct argparse *self,
                             const struct argparse_option *opt, int flags) {
  const char *s = NULL;
  if (!opt->value) goto skipped;
  switch (opt->type) {
    case ARGPARSE_OPT_BOOLEAN:
      if (flags & OPT_UNSET) {
        *(int *) opt->value = *(int *) opt->value - 1;
      } else {
        *(int *) opt->value = *(int *) opt->value + 1;
      }
      if (*(int *) opt->value < 0) { *(int *) opt->value = 0; }
      break;
    case ARGPARSE_OPT_BIT:
      if (flags & OPT_UNSET) {
        *(int *) opt->value &= ~opt->data;
      } else {
        *(int *) opt->value |= opt->data;
      }
      break;
    case ARGPARSE_OPT_STRING:
      if (self->optvalue) {
        *(const char **) opt->value = self->optvalue;
        self->optvalue = NULL;
      } else if (self->argc > 1) {
        self->argc--;
        *(const char **) opt->value = *++self->argv;
      } else {
        argparse_error(self, opt, "requires a value", flags);
      }
      break;
    case ARGPARSE_OPT_INTEGER:
      errno = 0;
      if (self->optvalue) {
        *(int *) opt->value = strtol(self->optvalue, (char **) &s, 0);
        self->optvalue = NULL;
      } else if (self->argc > 1) {
        self->argc--;
        *(int *) opt->value = strtol(*++self->argv, (char **) &s, 0);
      } else {
        argparse_error(self, opt, "requires a value", flags);
      }
      if (errno == ERANGE)
        argparse_error(self, opt, "numerical result out of range", flags);
      if (s[0] != '\0')// no digits or contains invalid characters
        argparse_error(self, opt, "expects an integer value", flags);
      break;
    case ARGPARSE_OPT_FLOAT:
      errno = 0;
      if (self->optvalue) {
        *(float *) opt->value = strtof(self->optvalue, (char **) &s);
        self->optvalue = NULL;
      } else if (self->argc > 1) {
        self->argc--;
        *(float *) opt->value = strtof(*++self->argv, (char **) &s);
      } else {
        argparse_error(self, opt, "requires a value", flags);
      }
      if (errno == ERANGE)
        argparse_error(self, opt, "numerical result out of range", flags);
      if (s[0] != '\0')// no digits or contains invalid characters
        argparse_error(self, opt, "expects a numerical value", flags);
      break;
    default:
      assert(0);
  }
skipped:
  if (opt->callback) { return opt->callback(self, opt); }
  return 0;
}
static void argparse_options_check(const struct argparse_option *options) {
  for (; options->type != ARGPARSE_OPT_END; options++) {
    switch (options->type) {
      case ARGPARSE_OPT_END:
      case ARGPARSE_OPT_BOOLEAN:
      case ARGPARSE_OPT_BIT:
      case ARGPARSE_OPT_INTEGER:
      case ARGPARSE_OPT_FLOAT:
      case ARGPARSE_OPT_STRING:
      case ARGPARSE_OPT_GROUP:
        continue;
      default:
        fprintf(stderr, str_literal("wrong option type: %d"), options->type);
        break;
    }
  }
}
static int argparse_short_opt(struct argparse *self,
                              const struct argparse_option *options) {
  for (; options->type != ARGPARSE_OPT_END; options++) {
    if (options->short_name == *self->optvalue) {
      self->optvalue = self->optvalue[1] ? self->optvalue + 1 : NULL;
      return argparse_getvalue(self, options, 0);
    }
  }
  return -2;
}
static int argparse_long_opt(struct argparse *self,
                             const struct argparse_option *options) {
  for (; options->type != ARGPARSE_OPT_END; options++) {
    const char *rest;
    int opt_flags = 0;
    if (!options->long_name) continue;
    rest = prefix_skip(self->argv[0] + 2, options->long_name);
    if (!rest) {
      // negation disabled?
      if (options->flags & OPT_NONEG) { continue; }
      // only OPT_BOOLEAN/OPT_BIT supports negation
      if (options->type != ARGPARSE_OPT_BOOLEAN &&
          options->type != ARGPARSE_OPT_BIT) {
        continue;
      }
      if (prefix_cmp(self->argv[0] + 2, "no-")) { continue; }
      rest = prefix_skip(self->argv[0] + 2 + 3, options->long_name);
      if (!rest) continue;
      opt_flags |= OPT_UNSET;
    }
    if (*rest) {
      if (*rest != '=') continue;
      self->optvalue = rest + 1;
    }
    return argparse_getvalue(self, options, opt_flags | OPT_LONG);
  }
  return -2;
}
int argparse_init(struct argparse *self, struct argparse_option *options,
                  const char *const *usages, int flags) {
  memset(self, 0, sizeof(*self));
  self->options = options;
  self->usages = usages;
  self->flags = flags;
  self->description = NULL;
  self->epilog = NULL;
  return 0;
}
void argparse_describe(struct argparse *self, const char *description,
                       const char *epilog) {
  self->description = description;
  self->epilog = epilog;
}
int argparse_parse(struct argparse *self, int argc, const char **argv) {
  self->argc = argc - 1;
  self->argv = argv + 1;
  self->out = argv;
  argparse_options_check(self->options);
  for (; self->argc; self->argc--, self->argv++) {
    const char *arg = self->argv[0];
    if (arg[0] != '-' || !arg[1]) {
      if (self->flags & ARGPARSE_STOP_AT_NON_OPTION) { goto end; }
      // if it's not option or is a single char '-', copy verbatim
      self->out[self->cpidx++] = self->argv[0];
      continue;
    }
    // short option
    if (arg[1] != '-') {
      self->optvalue = arg + 1;
      switch (argparse_short_opt(self, self->options)) {
        case -1:
          break;
        case -2:
          goto unknown;
      }
      while (self->optvalue) {
        switch (argparse_short_opt(self, self->options)) {
          case -1:
            break;
          case -2:
            goto unknown;
        }
      }
      continue;
    }
    // if '--' presents
    if (!arg[2]) {
      self->argc--;
      self->argv++;
      break;
    }
    // long option
    switch (argparse_long_opt(self, self->options)) {
      case -1:
        break;
      case -2:
        goto unknown;
    }
    continue;
  unknown:
#if defined(_WIN32) || defined(_WIN64)
  {
    wchar_t *unknown_arg_copy =
        yk__utf8_to_utf16_null_terminated(self->argv[0]);
    if (unknown_arg_copy != NULL) {
      fprintf(stderr, str_literal("error: unknown option `%ls`\n"),
              self->argv[0]);
    }
    free(unknown_arg_copy);
  }
#else
    fprintf(stderr, str_literal("error: unknown option `%s`\n"), self->argv[0]);
#endif
    argparse_usage(self);
    if (!(self->flags & ARGPARSE_IGNORE_UNKNOWN_ARGS)) { exit(EXIT_FAILURE); }
  }
end:
  memmove(self->out + self->cpidx, self->argv, self->argc * sizeof(*self->out));
  self->out[self->cpidx + self->argc] = NULL;
  return self->cpidx + self->argc;
}
void argparse_usage(struct argparse *self) {
  if (self->usages) {
#if defined(_WIN32) || defined(_WIN64)
    const char* usages_orig = *self->usages++;
    wchar_t *usages_copy = yk__utf8_to_utf16_null_terminated(usages_orig);
    if (usages_copy != NULL) {
      fprintf(stdout, str_literal("Usage: %ls\n"), usages_copy);
    }
    free(usages_copy);
#else
    fprintf(stdout, str_literal("Usage: %s\n"), *self->usages++);
#endif
    while (*self->usages && **self->usages) {
#if defined(_WIN32) || defined(_WIN64)
      wchar_t *usages_copy_2 =
          yk__utf8_to_utf16_null_terminated(*self->usages++);
      if (usages_copy_2 != NULL) {
        fprintf(stdout, str_literal("   or: %ls\n"), usages_copy_2);
      }
      free(usages_copy_2);
#else
      fprintf(stdout, str_literal("   or: %s\n"), *self->usages++);
#endif
    }
  } else {
    fprintf(stdout, str_literal("Usage:\n"));
  }
  // print description
  if (self->description) {
#if defined(_WIN32) || defined(_WIN64)
    wchar_t *description_copy =
        yk__utf8_to_utf16_null_terminated(self->description);
    if (description_copy != NULL) {
      fprintf(stdout, str_literal("%ls\n"), description_copy);
    }
    free(description_copy);
#else
    fprintf(stdout, str_literal("%s\n"), self->description);
#endif
  }
  fputc(str_literal('\n'), stdout);
  const struct argparse_option *options;
  // figure out best width
  size_t usage_opts_width = 0;
  size_t len;
  options = self->options;
  for (; options->type != ARGPARSE_OPT_END; options++) {
    len = 0;
    if ((options)->short_name) { len += 2; }
    if ((options)->short_name && (options)->long_name) {
      len += 2;// separator ", "
    }
    if ((options)->long_name) { len += strlen((options)->long_name) + 2; }
    if (options->type == ARGPARSE_OPT_INTEGER) { len += strlen("=<int>"); }
    if (options->type == ARGPARSE_OPT_FLOAT) {
      len += strlen("=<flt>");
    } else if (options->type == ARGPARSE_OPT_STRING) {
      len += strlen("=<str>");
    }
    len = (len + 3) - ((len + 3) & 3);
    if (usage_opts_width < len) { usage_opts_width = len; }
  }
  usage_opts_width += 4;// 4 spaces prefix
  options = self->options;
  for (; options->type != ARGPARSE_OPT_END; options++) {
    size_t pos = 0;
    size_t pad = 0;
    if (options->type == ARGPARSE_OPT_GROUP) {
      fputc(str_literal('\n'), stdout);
#if defined(_WIN32) || defined(_WIN64)
      wchar_t *help_copy_1 = yk__utf8_to_utf16_null_terminated(options->help);
      if (help_copy_1 != NULL) {
        fprintf(stdout, str_literal("%ls"), help_copy_1);
      }
      free(help_copy_1);
#else
      fprintf(stdout, str_literal("%s"), options->help);
#endif
      fputc(str_literal('\n'), stdout);
      continue;
    }
    pos = fprintf(stdout, str_literal("    "));
    if (options->short_name) {
      pos += fprintf(stdout, str_literal("-%c"), options->short_name);
    }
    if (options->long_name && options->short_name) {
      pos += fprintf(stdout, str_literal(", "));
    }
    if (options->long_name) {
#if defined(_WIN32) || defined(_WIN64)
      wchar_t *long_name_copy_1 =
          yk__utf8_to_utf16_null_terminated(options->long_name);
      if (long_name_copy_1 != NULL) {
        pos += fprintf(stdout, str_literal("--%ls"), long_name_copy_1);
      }
      free(long_name_copy_1);
#else
      pos += fprintf(stdout, str_literal("--%s"), options->long_name);
#endif
    }
    if (options->type == ARGPARSE_OPT_INTEGER) {
      pos += fprintf(stdout, str_literal("=<int>"));
    } else if (options->type == ARGPARSE_OPT_FLOAT) {
      pos += fprintf(stdout, str_literal("=<flt>"));
    } else if (options->type == ARGPARSE_OPT_STRING) {
      pos += fprintf(stdout, str_literal("=<str>"));
    }
    if (pos <= usage_opts_width) {
      pad = usage_opts_width - pos;
    } else {
      fputc(str_literal('\n'), stdout);
      pad = usage_opts_width;
    }
#if defined(_WIN32) || defined(_WIN64)
    wchar_t *help_copy = yk__utf8_to_utf16_null_terminated(options->help);
    if (help_copy != NULL) {
      fprintf(stdout, str_literal("%*ls%ls\n"), (int) pad + 2, str_literal(""),
              help_copy);
    }
    free(help_copy);
#else
    fprintf(stdout, str_literal("%*s%s\n"), (int) pad + 2, str_literal(""),
            options->help);
#endif
  }
  // print epilog
  if (self->epilog) {
#if defined(_WIN32) || defined(_WIN64)
    wchar_t *epilog_copy = yk__utf8_to_utf16_null_terminated(self->epilog);
    if (epilog_copy != NULL) {
      fprintf(stdout, str_literal("%ls\n"), epilog_copy);
    }
    free(epilog_copy);
#else
    fprintf(stdout, str_literal("%s\n"), self->epilog);
#endif
  }
}
int argparse_help_cb_no_exit(struct argparse *self,
                             const struct argparse_option *option) {
  (void) option;
  argparse_usage(self);
  return (EXIT_SUCCESS);
}
int argparse_help_cb(struct argparse *self,
                     const struct argparse_option *option) {
  argparse_help_cb_no_exit(self, option);
  exit(EXIT_SUCCESS);
}
